/*
 * Developer : Jack Matthews
 * Email: developer@droidprofessor.com
 * 
 * Copyright (C) 2010 http://droidprofessor.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.droidprofessor.android.library.phonelicenses;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.http.protocol.HTTP;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;

import android.util.Log;


/**
 * Manages XML parsing
 * 
 * @author Jack Matthews
 *
 *
 * 
 */
class XmlParser {

	private static final String TAG = "XmlParser";
	private XmlParser(){};

	/**
	 * Parse an Application response
	 * 
	 * @param in
	 * @return XmlResponse<ArrayList<HashMap<String, String>>>
	 */
	static XmlResponse<Void> parseXmlApplication(InputStream in) {
		XmlParser.XmlApplicationHandler innerXml = new XmlParser.XmlApplicationHandler();
		XmlResponseHandler<Void> outerXml = new XmlResponseHandler<Void>(innerXml, in);
		return outerXml.getResponse();
	}



	/**
	 * Parse a license resposne
	 * 
	 * @param in
	 * @return XmlResponse<ArrayList<HashMap<String, String>>>
	 */
	static XmlResponse<String> parseXmlLicense(InputStream in) {
		XmlParser.XmlLicenseHandler innerXml = new XmlParser.XmlLicenseHandler();
		XmlResponseHandler<String> outerXml = new XmlResponseHandler<String>(innerXml, in);
		return outerXml.getResponse();
	}



	/**
	 * Parse a simple response XML resource
	 * @param in
	 * @return XmlResponse<VoidXmlParsable>
	 */
	static XmlResponse<Void> parseXmlResponse(InputStream in) {
		XmlParser.VoidXmlParsable innerXml = new XmlParser.VoidXmlParsable();
		XmlResponseHandler<Void> outerXml = new XmlResponseHandler<Void>(innerXml, in);
		return outerXml.getResponse();
	}




	/**
	 * This is the base parsing class that will generate XmlResponse
	 * 
	 * T is the expected return object
	 * K is the Xml Handler
	 * 
	 * @return XmlResponse<T>
	 */
	private static class XmlResponseHandler<T> {

		private XmlResponse<T> xmlResponse;

		public <K extends XmlParsable<T>> XmlResponseHandler ( K handler, InputStream in) {

			try {
				BufferedInputStream bis = new BufferedInputStream(in);
				bis.mark(1024 * 1024);

				//parse the outer response
				parseOuterXml(bis);
				bis.reset();
				
				//Parse the data if there is no error
				if( ! xmlResponse.isError() ) 
					parseInnerXml(handler, bis); 

			} catch (IOException e) {
				Log.e(TAG, "Unable to reset the the BufferedInputStream", e);
				//this.xmlResponse = new XmlResponse<T>(0, "IOException", "An IOException Occurred while resetting the buffer");
			} catch (Throwable t) {
				Log.e(TAG, "An Exception Occurred while parsing the XML Document", t);
				//this.xmlResponse = new XmlResponse<T>(0, "Exception", "An Unhandled Exception Occurred: "+t.getMessage());
			}
		}

		/**
		 * Parse the inner XML data
		 * 
		 * @param in
		 */
		private void parseInnerXml(XmlParsable<T> handler, InputStream in) {
			handler.parse(in);
			this.xmlResponse.setObject(handler.getXmlAsObject());
		}


		/**
		 * Parse the outer response
		 * 
		 * Guaranteed to produce a XmlResponse object
		 * 
		 * @param in
		 * @throws Exception
		 */
		private void parseOuterXml(InputStream in) throws Exception {

			int httpCode = 0;
			String type = "Exception";
			String message = "The Document was not in Valid XML";
			final StringBuilder sb = new StringBuilder();

			try {
				XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
				factory.setNamespaceAware(true);
				//factory.setValidating(true);
				XmlPullParser xpp = factory.newPullParser();

				xpp.setInput(in, HTTP.UTF_8);

				boolean inMessage = false;

				int eventType = xpp.getEventType();
				while (eventType != XmlPullParser.END_DOCUMENT) {
					
					//START TAGS
					if(eventType == XmlPullParser.START_TAG) {
						if(xpp.getName().equals("message")) {
							httpCode = Integer.valueOf( xpp.getAttributeValue(null, "httpCode").trim() );
							type = xpp.getAttributeValue(null, "type").trim();
							inMessage = true;
						}
					
					//TAG TEXT
					} else if(eventType == XmlPullParser.TEXT) {
						if(inMessage) {
							sb.append(xpp.getText().trim());
						}
					
					//END TAGS
					} else if(eventType == XmlPullParser.END_TAG) {
						if(inMessage) {
							message = sb.toString();
							inMessage = false;
							
							//setup the response
							this.xmlResponse = new XmlResponse<T>(httpCode, type, message);
							
							//Can return now as we have all the data we need
							break; 
						}
					}

					eventType = xpp.next();
				}
			} catch (Exception e) {
				Log.e(TAG, "Cannot Parse outer XML", e);
				throw new Exception(e);
			} finally {
				//Guarantee that we have an XmlResposne Object
				this.xmlResponse = new XmlResponse<T>(httpCode, type, message);
			}
		} 



		/**
		 * Return the result
		 * 
		 * @return XmlResponse<T> the final parsed resposne
		 */
		public XmlResponse<T> getResponse() {
			return xmlResponse==null?
					new XmlResponse<T>(500, "Bad XML", "The XML Document did not parse correctly"):
						xmlResponse;
		}

	}






	/**
	 * XML Handing for Application Data
	 */
	private static class XmlApplicationHandler extends XmlParsableSaxDefaultHandler<Void> {
		private boolean 
			inApplication,
			inName,
			inDescription,
			inSupportEmail,
			inPhoneProductsUrl,
			inMerchantName,
			inLicenseAgreement,
			inRefundPolicy,
			inBaseUrl,
			inPaymentMethod,
			inPrice,
			inButton,
			inExtras
			= false;
			
		private final StringBuilder tagString = new StringBuilder();
		private ApplicationDataCache.PaymentMethod paymentMethod;
		private String tagName; //used for extras
		
		public XmlApplicationHandler() {
			//Clear out the old informtion
			ApplicationDataCache.getInstance().reset();
		}

		@Override
		public void startElement(String uri, String localName, String qName,
				Attributes attributes) throws SAXException {
			tagName = localName; //for use later			
			
			//Application DATA
			if(localName.equals("application")) this.inApplication = true;
			else if(localName.equals("paymentMethod")) {
				this.inPaymentMethod = true;
				this.paymentMethod = new ApplicationDataCache.PaymentMethod();
				boolean enabled = Boolean.valueOf(attributes.getValue("enabled"));
				this.paymentMethod.setEnabled(enabled);
				this.paymentMethod.setRefName(attributes.getValue("refName"));
			}
			else if(localName.equals("name")) this.inName = true;
			else if(localName.equals("description")) this.inDescription = true;
			else if(localName.equals("supportEmail")) this.inSupportEmail = true;
			else if(localName.equals("phoneProductsUrl")) this.inPhoneProductsUrl = true;
			else if(localName.equals("merchantName")) this.inMerchantName = true;
			else if(localName.equals("licenseAgreement")) this.inLicenseAgreement = true;
			else if(localName.equals("refundPolicy")) this.inRefundPolicy = true;
			else if(localName.equals("baseUrl")) this.inBaseUrl = true;
			else if(localName.equals("price")) {
				this.inPrice = true;
				this.paymentMethod.setCurrency(attributes.getValue("currency"));
			}
			else if(localName.equals("button")) {
				this.inButton = true;
				this.paymentMethod.setButtonType(attributes.getValue("type"));
			}
			else if(localName.equals("extras")) this.inExtras = true;
		}


		@Override
		public void characters(char[] ch, int start, int length)
		throws SAXException {
			String extractedString = new String(ch, start, length);
			tagString.append(extractedString); //allows multiple reads
		}


		@Override
		public void endElement(String uri, String localName, String qName)
		throws SAXException {
			//Application DATA
			if(localName.equals("application") && inApplication) {
				this.inApplication = false;
			}
			else if(localName.equals("paymentMethod") && inPaymentMethod) {
				this.inPaymentMethod = false;
				ApplicationDataCache.getInstance().addPaymentMethod(this.paymentMethod);
				this.paymentMethod = null; //remove the payment method
			}
			else if(localName.equals("name") && inName && !inPaymentMethod) {
				this.inName = false;
				ApplicationDataCache.getInstance().setApplicationName(this.tagString.toString().trim());
			}
			else if(localName.equals("name") && inName && inPaymentMethod) {
				this.inName = false;
				this.paymentMethod.setName(this.tagString.toString().trim());
			}
			else if(localName.equals("description") && inDescription && !inPaymentMethod) {
				this.inDescription = false;
				ApplicationDataCache.getInstance().setDescription(this.tagString.toString().trim());
			}
			else if(localName.equals("description") && inDescription && inPaymentMethod) {
				this.inDescription = false;
				this.paymentMethod.setDescription(this.tagString.toString().trim());
			}
			else if(localName.equals("supportEmail") && inSupportEmail) {
				this.inSupportEmail = false;
				ApplicationDataCache.getInstance().setSupportEmail(this.tagString.toString().trim());
			}
			else if(localName.equals("phoneProductsUrl") && inPhoneProductsUrl) {
				this.inPhoneProductsUrl = false;
				ApplicationDataCache.getInstance().setPhoneProductsUrl(this.tagString.toString().trim());
			}
			else if(localName.equals("merchantName") && inMerchantName) {
				this.inMerchantName = false;
				ApplicationDataCache.getInstance().setMerchantName(this.tagString.toString().trim());
			}
			else if(localName.equals("licenseAgreement") && inLicenseAgreement) {
				this.inLicenseAgreement = false;
				ApplicationDataCache.getInstance().setLicenseAgreement(this.tagString.toString().trim());
			}
			else if(localName.equals("refundPolicy") && inRefundPolicy) {
				this.inRefundPolicy = false;
				ApplicationDataCache.getInstance().setRefundPolicy(this.tagString.toString().trim());
			}
			else if(localName.equals("baseUrl") && inBaseUrl) {
				this.inBaseUrl = false;
				ApplicationDataCache.getInstance().setBaseUrl(this.tagString.toString().trim());
			}
			else if(localName.equals("price") && inPrice) {
				this.inPrice = false;
				this.paymentMethod.setPrice(this.tagString.toString().trim());
			}
			else if(localName.equals("button") && inButton) {
				this.inButton = false;
				this.paymentMethod.setButtonInformation(this.tagString.toString().trim());
			}
			else if(localName.equals("extras")) {
				this.inExtras = false;
			}
			else if(inExtras) { //All fields inside the extras block
				this.paymentMethod.addExtra(this.tagName, this.tagString.toString().trim());
			}

			
			//Reset the string builder
			this.tagString.delete(0, this.tagString.length());
		}


		@Override
		public Void getXmlAsObject() {
			return null;
		}


	}





	/**
	 * A dummy class for responses that contain no innerXml such
	 * as error messages and simple messages from the server.
	 * 
	 * @author Jack Matthews
	 *
	 */
	private static class VoidXmlParsable implements XmlParsable<Void> {

		@Override
		public Void getXmlAsObject() {
			return null;
		}

		@Override
		public void parse(InputStream in) {
			//do nothing
		}

	}





	/**
	 * Parse a license response
	 * 
	 * @author Jack Matthews
	 *
	 */
	private static class XmlLicenseHandler extends XmlParsableSaxDefaultHandler<String> {
		private final StringBuilder licenseData = new StringBuilder();
		private boolean inLicense = false;

		@Override
		public void startElement(String uri, String localName, String qName,
				Attributes attributes) throws SAXException {

			if(localName.equals("license")) {
				this.inLicense  = true;
			}
		}


		@Override
		public void characters(char[] ch, int start, int length)
		throws SAXException {
			String extractedString = new String(ch, start, length);

			//New Misspelling
			if (inLicense){
				//need to append in case the read did not get all the data on the first pass
				licenseData.append(extractedString);
			}
		}


		@Override
		public void endElement(String uri, String localName, String qName)
		throws SAXException {
			if(localName.equals("license")) {
				inLicense = false;	
			}

		}


		@Override
		public String getXmlAsObject() {
			return licenseData.toString();
		}


	}


}
